#!/usr/bin/python
#
# Copyright (c) 2009 Julius Volz
# See LICENSE for details.

import silk, config


class Error(silk.Error):
  """Used to signal errors during RDF Path evaluation."""

class PathOperator(object):
  # the current "result" variable that the next operator should operate on,
  # is incremented for each path navigation step ('/' or '\')
  current_var = 0


class TraversalOperator(PathOperator):
  def __init__(self, property, backward=False):
    self.property = property
    self.backward = backward

  def sparql(self, subject=None):
    if subject is None:
      subject = '?var_%s' % self.current_var

    object = '?var_%s' % (self.current_var + 1)

    if self.backward:
      subject, object = object, subject

    PathOperator.current_var += 1

    return '%s %s %s .\n' % (subject, self.property, object)


class Filter(PathOperator):
  filter_operators = ['<', '>', '<=', '>=', '=', '!=']

  def __init__(self, operator):
    if operator not in self.filter_operators:
      raise Error('Invalid filter operator "%s" used in RDF Path' % operator)
    self.operator = operator


class PropertyFilter(Filter):
  num_filters = 0

  def __init__(self, property, operator, value):
    super(PropertyFilter, self).__init__(operator)

    PropertyFilter.num_filters += 1
    self.filter_var = '?filter_%s' % self.num_filters
    self.property = property
    self.value = value

  def sparql(self, subject=None):
    if subject is None:
      subject = '?var_%s' % self.current_var

    sparql = '%s %s %s .\n' % (subject, self.property, self.filter_var)
    sparql += 'FILTER(%s %s %s) .\n' % (self.filter_var, self.operator, self.value)
    
    return sparql


class LangFilter(Filter):
  def __init__(self, operator, lang):
    super(LangFilter, self).__init__(operator)
    self.lang = lang

  def sparql(self, subject=None):
    if subject is None:
      subject = self.current_var

    return 'FILTER(lang(?var_%s) %s %s) . \n' % (subject, self.operator, self.lang)


class RDFPath(object):
  component_types = {
    '/': 'forward',
    '\\': 'backward',
    '[': 'filter',
    ']': 'end_filter'
  }

  def __init__(self, path):
    self.path = path
    self.components = []
    self.operators = []

    self.parse_path()
    self.parse_components()

  def parse_path(self):
    path = self.path

    in_uri = False
    in_quotes = False

    c_type = 'init'
    start = 0

    for i in xrange(0, len(path)):
      if path[i] == '<' and path[i - 1] != ' ':
        if in_uri:
          raise Error('Already in URI!')
        else:
          in_uri = True

      if path[i] == '>' and path[i - 1] != ' ':
        if not in_uri:
          raise Error('Not in URI!')
        else:
          in_uri = False

      if path[i] == '\'':
        if in_quotes:
          in_quotes = False
        else:
          in_quotes = True

      if c_type == 'end_filter':
        c_type = self.component_types[path[i]]
        continue

      if in_uri or in_quotes:
        continue

      # handle path operators
      if path[i] in ['/', '\\', '[', ']'] or i + 1 == len(path):
        if c_type == 'filter' and path[i] == '[':
          continue

        if i + 1 < len(path):
          self.components.append({ 'type': c_type, 'value': path[start:i] })

          if c_type == 'filter' and path[i] == ']':
            start = i + 2
          else:
            start = i + 1
          c_type = self.component_types[path[i]]
        else:
          if c_type == 'filter' and path[i] == ']':
            end = i
          else:
            end = i + 1
          self.components.append({ 'type': c_type, 'value': path[start:end] })

    if in_uri:
      raise Error('URI in path "%s" is not terminated correctly')

    if in_quotes:
      raise Error('Quoted string in path "%s" is not terminated correctly')


  def parse_components(self):
    self.var = self.components[0]['value']
    if not self.var.startswith('?'):
      raise Error('Path "%s" does not start with variable' % self.path)

    # cut off '?'
    self.var = self.var[1:]

    for comp in self.components[1:]:
      # filter operator
      if comp['type'] == 'filter':
        try:
          (property, operator, value) = comp['value'].split()
        except:
          raise Error('Broken filter condition: %s' % (comp['value']))
        
        # language filter
        if property.startswith('@'):
          if property != '@lang':
            raise Error('Unsupported attribute "%s" used for selection in RDF Path' % property)
          self.operators.append(LangFilter(operator, value))
        # property filter
        else:
          self.operators.append(PropertyFilter(property, operator, value))

      # forward, backward traversal
      else:
        property = comp['value']
        backward = False
        if comp['type'] == 'backward':
          backward = True
        self.operators.append(TraversalOperator(property, backward))

  # Not used at the moment
  def check_uri(self, uri):
    if uri[0] == '<':
      return uri[1:-1]
    else:
      try:
        prefix, element = uri.split(':')
        return config.prefixes[prefix] + element
      except:
        raise Error('Error parsing prefixed URI "%s"' % uri)

  def to_sparql(self, bindings, graph):
    where_clause = ''

    # TODO: allow sets of resources as first path component
    subject = '<%s>' % bindings[self.var][0]['value']

    PathOperator.current_var = 0
    for operator in self.operators:
      where_clause += operator.sparql(subject)
      subject = None

    prefixes = ''
    for prefix in config.prefixes:
      prefixes += 'PREFIX %s: <%s>\n' % (prefix, config.prefixes[prefix])

    graph_clause = ''
    if graph is not None:
      graph_clause = 'FROM <%s>' % graph

    sparql = """
      %s
      SELECT DISTINCT ?var_%s
      %s
      WHERE
      {
        %s
      }""" % (prefixes, PathOperator.current_var, graph_clause, where_clause)
    return sparql

  def evaluate(self, bindings):
    if len(self.components) == 1:
      return bindings[self.var]

    # find out dataset to use from resource variable in first path element
    ds = bindings[self.var][0]['dataset']

    sparql = self.to_sparql(bindings, ds.get_graph())

    results = ds.query(sparql)
    return [x.values()[0] for x in results['bindings']]
